package net.wanderinghorse.jsonmessage.dispatch;

import net.wanderinghorse.jsonmessage.Request;
import net.wanderinghorse.jsonmessage.RequestHandler;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import java.util.HashSet;
import java.util.Set;

/**
 * This servlet class acts as a dispatcher for a single RequestHandler type.
 * It expects a JSON-formatted request object as input, but will fail
 * if the input request is not of the configured type.
 *
 * Limitations:
 *
 * - This servlet assumes that all requests sent to it are of a specific
 * type (as determined via Request.getType()). It may misbehave if
 * the incoming requests are not of the type it is configured to handle.
 *
 * Requirements:
 *
 * - Either it must be configured via web.xml (see below) or passed the name
 * of a RequestHandler class to its constructor, or passed a RequestHandler
 * to its other constructor.
 *
 * - web.xml: An init-param with the name "requestHandlerClass" and a value
 * holding a fully-qualified RequestHandler class name. An init-param named
 * "alternateTypeNames" with a space- or comma-separated list of alternate
 * request type name aliases which should be regarded as valid. Requests
 * which have a type name other than the requestHandlerClass or something
 * in the alternateTypeNames list will be rejected. By default the servlet
 * responds to requests with the type specified by requestHandlerClass, or
 * to the base-name form of that class (everything after the final period).
 *
 * The following web.xml example sets up a servlet which responds to Ping requests:
 *
 * <pre>
 *     <servlet>
        <servlet-name>jsonmessage-response-ping</servlet-name>
        <servlet-class>net.wanderinghorse.jsonmessage.dispatch.SingleRequestDispatcherServlet</servlet-class>
        <init-param>
            <param-name>requestHandlerClass</param-name>
            <param-value>net.wanderinghorse.jsonmessage.response.Ping</param-value>
        </init-param>
        <init-param>
            <param-name>alternateTypeNames</param-name>
            <param-value>ping</param-value>
        </init-param>
      </servlet>

    <servlet-mapping>
        <servlet-name>jsonmessage-response-ping</servlet-name>
        <url-pattern>/ping</url-pattern>
    </servlet-mapping>
 * </pre>
 */
public class SingleRequestDispatcherServlet extends RequestDispatcherServlet {

    private RequestHandler handler;
    private Set<String> typeNames = new HashSet<String>();
    private static final String KEY_HANDLER_CLASS = "requestHandlerClass";
    private static final String KEY_TYPE_NAMES = "alternateTypeNames";

    public SingleRequestDispatcherServlet(){
    }

    public SingleRequestDispatcherServlet(final String handlerClass) throws ServletException {
        init(handlerClass);
    }
    public SingleRequestDispatcherServlet(final RequestHandler handler) {
        if( null == handler) {
            throw new IllegalArgumentException(getClass().getName()
                    +" constructor does not accept a null RequestHandler");
        }
        this.handler = handler;
    }

    @Override
    protected RequestHandler loadRequestHandler(final Request request) {
//        if( null == request.getType() ) {
//            request.setType( handler.getClass().getName() );
//        }
        if( !typeNames.contains(request.getType()) ) {
            throw new IllegalArgumentException("This "+getClass().getName()
                    +" instance is not configured to allow requests of type ["+request.getType()+"].");
        }
        return handler;
    }

    /**
     * Adds the given name, which may not be null, as an "acceptable"
     * request type for this servlet.
     * @param typeName An alternate request type name, which presumably is
     * an alias for this instance's RequestHandler class.
     */
    public void addAlternateTypeName( final String typeName ) {
        typeNames.add( typeName );
    }

    /**
     * Tries to load a new RequestHandler instance from the given class name.
     *
     * @param hclass A fully-qualified RequestHandler class name.
     * @throws ServletException If no instance can be instantiated. It throws
     * this type of exception because it will normally be called via
     * init(ServletConfig).
     */
    protected void init(final String hclass) throws ServletException {
        if( null == hclass || hclass.isEmpty() ) {
            throw new ServletException(KEY_HANDLER_CLASS+" servlet parameter is not set!");
        }
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        if( null == cl /* is this possible? The API docs are not specific. */) {
            throw new ServletException("Could not get classloader!");
        }
        Class c = null;
        try { c = cl.loadClass(hclass); }
        catch( ClassNotFoundException cnf ) {
            throw new ServletException("Could not load class ["+hclass+"].",cnf);
        }
        try {
            handler = (RequestHandler) c.newInstance();
        }
        catch(Exception e) {
            throw new ServletException("Could not instantiate new instance of ["+hclass+"].",e);
        }
        addAlternateTypeName( hclass );
        if( hclass.contains(".") ) {
            // Add base class name to the alternate names list.
            final String parts[] = hclass.split("\\.");
            addAlternateTypeName( parts[parts.length-1] );
        }
    }

    @Override
    public void init(final ServletConfig config) throws ServletException {
        super.init(config);
        init( config.getInitParameter(KEY_HANDLER_CLASS) );
        final String allowed = config.getInitParameter(KEY_TYPE_NAMES);
        if( null != allowed ) {
            String[] ar = allowed.split("[,\\s]");
            if( null != ar ) for( String s : ar ) {
                if( null != s && !s.isEmpty() ) {
                    addAlternateTypeName( s );
                }
            }
        }
    }
    
}
